﻿using System;
using System.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Navigation;

namespace PrincetonSUG.UserGroupSite.Helpers
{
	/// <summary>
	/// Collection of methods for working with <see cref="SPWeb"/>.
	/// </summary>
	public static class SPWebHelper
	{
		/// <summary>
		/// Find or add a list to a site.
		/// </summary>
		/// <param name="site">The site to find or add a list</param>
		/// <param name="title">The title of the list.</param>
		/// <param name="description">The description of the list.</param>
		/// <param name="template">The template to base the list on.</param>
		/// <returns>The new or found list.</returns>
		public static SPList EnsureList(SPWeb site, string title, string description, SPListTemplateType template)
		{
			SPList list = FindListByTitleOrDefault(site, title);
			if (list == null)
			{
				Guid listGuid = site.Lists.Add(title, description, template);
				list = site.Lists[listGuid];
			}
			return list;
		}

		/// <summary>
		/// Find or add a list to a site.
		/// </summary>
		/// <param name="site">The site to find or add a list</param>
		/// <param name="title">The title of the list.</param>
		/// <param name="description">The description of the list.</param>
		/// <param name="template">The template to base the list on.</param>
		/// <returns>The new or found list.</returns>
		public static SPList EnsureList(SPWeb site, string title, string description, SPListTemplate template)
		{
			SPList list = FindListByTitleOrDefault(site, title);
			if (list == null)
			{
				Guid listGuid = site.Lists.Add(title, description, template);
				list = site.Lists[listGuid];
			}
			return list;
		}

		/// <summary>
		/// Finds a list on a site or null if not found.
		/// </summary>
		/// <param name="site">The site to find the list in.</param>
		/// <param name="title">The title of the list to find.</param>
		/// <returns>The list or null if not found.</returns>
		public static SPList FindListByTitleOrDefault(SPWeb site, string title)
		{
			return site.Lists.Cast<SPList>().FirstOrDefault(l => string.Equals(l.Title, title, StringComparison.InvariantCultureIgnoreCase));
		}

		/// <summary>
		/// Finds a content type in this site or any parent site.
		/// </summary>
		/// <param name="site">The site to start the search.</param>
		/// <param name="group">The name of the group the content type belongs to.</param>
		/// <param name="name">The name of the content type.</param>
		/// <returns>The content type.</returns>
		public static SPContentType FindContentTypeByNameOrDefault(SPWeb site, string group, string name)
		{
			return FindInSiteOrParentSites(site, s => s.ContentTypes.Cast<SPContentType>()
				.FirstOrDefault(ct => string.Equals(ct.Name, name, StringComparison.InvariantCultureIgnoreCase)));
		}

		/// <summary>
		/// Finds or adds a content type.
		/// </summary>
		/// <param name="site">The site to find the content type in.</param>
		/// <param name="group">The name of the group the content type belongs to.</param>
		/// <param name="name">The name of the content type.</param>
		/// <param name="parentContentTypeId">The parent content type.</param>
		/// <returns>The content type.</returns>
		public static SPContentType EnsureContentType(SPWeb site, string group, string name, SPContentTypeId parentContentTypeId)
		{
			SPContentType contentType = FindContentTypeByNameOrDefault(site, group, name);
			if (contentType == null)
			{
				SPContentType parentContentType;
				if (parentContentTypeId == default(SPContentTypeId))
				{
					parentContentType = null;
				}
				else
				{
					parentContentType = FindContentTypeById(site, parentContentTypeId);
					if (parentContentType == null)
					{
						throw new ApplicationException("could not find content type '" + parentContentType + "'");
					}
				}
				contentType = new SPContentType(parentContentType, site.ContentTypes, name)
				{
					Group = group,
				};
				site.ContentTypes.Add(contentType);
			}
			return contentType;
		}

		/// <summary>
		/// Finds the content typ by id in this site or the parent site.
		/// </summary>
		/// <param name="site">The site to start searching</param>
		/// <param name="contentTypeId">the id of the content type to find</param>
		/// <returns>the content type or null if not found.</returns>
		public static SPContentType FindContentTypeById(SPWeb site, SPContentTypeId contentTypeId)
		{
			return FindInSiteOrParentSites(site, s =>
			{
				SPContentType ct = s.ContentTypes[contentTypeId];
				if (ct != null)
				{
					return ct;
				}
				return null;
			});
		}

		/// <summary>
		/// Finds or adds a lookup field.
		/// </summary>
		/// <param name="site">The site to find or add the lookup field.</param>
		/// <param name="id">The id to use for the field</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="name">The name of the lookup field.</param>
		/// <param name="lookupListId">The list the field looks up.</param>
		/// <param name="required">True, if field is required.</param>
		/// <returns>The field.</returns>
		public static SPFieldLookup EnsureFieldLookup(SPWeb site, Guid id, string group, string name, Guid lookupListId, bool required)
		{
			return EnsureFieldLookup(site, id, group, name, "Lookup", lookupListId, site.ID, required);
		}

		/// <summary>
		/// Finds or adds a lookup field.
		/// </summary>
		/// <param name="site">The site to find or add the lookup field.</param>
		/// <param name="id">The id to use for the field</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="name">The name of the lookup field.</param>
		/// <param name="type">Typically 'Lookup'.  It would only be something else if you had a custom field type that derived from the Lookup type.</param>
		/// <param name="webId">The id of the web that the parent list lives in.</param>
		/// <param name="lookupListId">The list the field looks up.</param>
		/// <param name="required">True, if field is required.</param>
		/// <returns>The field.</returns>
		public static SPFieldLookup EnsureFieldLookup(SPWeb site, Guid id, string group, string name, string type, Guid lookupListId, Guid webId, bool required)
		{
			SPFieldLookup field = (SPFieldLookup)FindFieldByIdOrNameDefault(site, id, name);
			if (field == null)
			{
				string xml = "<Field DisplayName='" + name + "' Type='" + type + "' Required='" + (required ? "TRUE" : "FALSE") + "' List='" + GuidHelper.ToSPString(lookupListId) + "' WebId='" + GuidHelper.ToSPString(webId) + "' ID='" + GuidHelper.ToSPString(id) + "' Name='" + name + "' Group='" + group + "' />";
				site.Fields.AddFieldAsXml(xml);
				field = (SPFieldLookup)site.Fields[name];
				field.Group = group;
				field.Update(true);
				field = (SPFieldLookup)FindFieldByIdOrNameDefault(site, id, name);
			}
			else
			{
				field.Required = required;
				field.Group = group;
				field.Update(true);
				field = (SPFieldLookup)site.Fields[field.Id];
			}
			return field;
		}


		/// <summary>
		/// Finds or adds a user field.
		/// </summary>
		/// <param name="site">The site to find or add the lookup field.</param>
		/// <param name="id">The id to use for the field</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="name">The name of the lookup field.</param>
		/// <param name="required">True, if field is required.</param>
		/// <returns>The field.</returns>
		public static SPFieldLookup EnsureFieldUser(SPWeb site, Guid id, string group, string name, bool required)
		{
			SPFieldLookup field = (SPFieldUser)FindFieldByIdOrNameDefault(site, id, name);
			if (field == null)
			{
				string xml = string.Format("<Field DisplayName='{0}' Type='User' Required='{1}' List='UserInfo' WebId='{2}' ID='{3}' Name='{4}' Group='{5}' ShowField='ImnName' UserSelectionMode='0' UserSelectionScope='0' />",
					name, (required ? "TRUE" : "FALSE"),
					GuidHelper.ToSPString(site.ID),
					GuidHelper.ToSPString(id),
					name,
					group
					);
				site.Fields.AddFieldAsXml(xml);
				field = (SPFieldUser)site.Fields[name];
				field.Group = group;
				field.Update(true);
				field = (SPFieldLookup)FindFieldByIdOrNameDefault(site, id, name);
			}
			else
			{
				field.Required = required;
				field.Group = group;
				field.Update(true);
				field = (SPFieldLookup)site.Fields[field.Id];
			}
			return field;
		}

		/// <summary>
		/// Finds or adds a field to a site.
		/// </summary>
		/// <param name="site">The site to find or add the field.</param>
		/// <param name="id">The id to use for the field</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="name">The name of the field</param>
		/// <param name="type">The type of the field</param>
		/// <param name="required">True, if the field is required.</param>
		/// <returns>The field.</returns>
		public static SPField EnsureField(SPWeb site, Guid id, string group, string name, SPFieldType type, bool required)
		{
			SPField field = FindFieldByIdOrNameDefault(site, id, name);
			if (field == null)
			{
				string typeName = site.FieldTypeDefinitionCollection[Enum.GetName(typeof(SPFieldType), type)].TypeName;
				string xml = string.Format("<Field Type='{0}' DisplayName='{1}' Group='{2}' ID='{3}' Name='{4}' />", typeName, name, group, GuidHelper.ToSPString(id), name);
				site.Fields.AddFieldAsXml(xml);
				field = FindFieldByIdOrNameDefault(site, id, name);
			}
			else
			{
				field.Required = required;
				field.Type = type;
				field.Group = group;
				field.Update(true);
				field = site.Fields.ContainsField(name) ? site.Fields[field.Id] : site.Site.RootWeb.Fields[field.Id];
			}

			return field;
		}

		/// <summary>
		/// Finds or adds a field to a site.
		/// </summary>
		/// <param name="site">The site to find or add the field to.</param>
		/// <param name="id">The id to use for the field</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="name">The name of the field.</param>
		/// <param name="typeName">The type name of the field.</param>
		/// <returns>The field.</returns>
		public static SPField EnsureField(SPWeb site, Guid id, string group, string name, string typeName)
		{
			SPField field = FindFieldByIdOrNameDefault(site, id, name);
			if (field == null)
			{
				string xml = "<Field Type='" + typeName + "' DisplayName='" + name + "' Group='" + group + "' ID='" + GuidHelper.ToSPString(id) + "' Name='" + name + "' />";
				site.Fields.AddFieldAsXml(xml);
				field = FindFieldByIdOrNameDefault(site, id, name);
			}
			return field;
		}

		/// <summary>
		/// Finds an object on the given site or any of it's parent sites.
		/// </summary>
		/// <typeparam name="TResult">The object type to find.</typeparam>
		/// <param name="site">The site to begin the search</param>
		/// <param name="finder">The delegate called at each level to find the object. If the object is not found this delegate should return null.</param>
		/// <returns>The object found or null if not found.</returns>
		public static TResult FindInSiteOrParentSites<TResult>(SPWeb site, Func<SPWeb, TResult> finder) where TResult : class
		{
			TResult result = finder(site);
			if (result != null)
			{
				return result;
			}
			if (site.ParentWeb == null)
			{
				return null;
			}
			return FindInSiteOrParentSites(site.ParentWeb, finder);
		}

		/// <summary>
		/// Finds a field in a group by name. If not found throws an exception.
		/// </summary>
		/// <param name="site">The site to find the field in</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="title">The title, static name, or internal name of the field.</param>
		/// <returns>The field.</returns>
		public static SPField FindFieldByGroupAndTitle(SPWeb site, string group, string title)
		{
			SPField field = FindFieldByGroupAndTitleOrDefault(site, group, title);
			if (field == null)
			{
				throw new ApplicationException("Could not find field '" + title + "' in group '" + group + "'");
			}
			return field;
		}

		/// <summary>
		/// Finds a field in a group by name.
		/// </summary>
		/// <param name="site">The site to find the field in</param>
		/// <param name="group">The group the field belongs to.</param>
		/// <param name="title">The title, static name, or internal name of the field.</param>
		/// <returns>The field or null if not found.</returns>
		public static SPField FindFieldByGroupAndTitleOrDefault(SPWeb site, string group, string title)
		{
			return FindInSiteOrParentSites(site, s => s.Fields.Cast<SPField>()
				.FirstOrDefault(f => string.Equals(f.Group, group, StringComparison.InvariantCultureIgnoreCase)
					&& (string.Equals(f.Title, title, StringComparison.InvariantCultureIgnoreCase)
					|| string.Equals(f.StaticName, title, StringComparison.InvariantCultureIgnoreCase)
					|| string.Equals(f.InternalName, title, StringComparison.InvariantCultureIgnoreCase))));
		}

		///<summary>
		/// Finds a field by id
		///</summary>
		///<param name="site">The site to start a search.</param>
		///<param name="id">The id of the field to find</param>
		///<returns>the field or null if not found.</returns>
		public static SPField FindFieldByIdOrDefault(SPWeb site, Guid id)
		{
			return FindFieldByIdOrNameDefault(site, id, null);
		}

		///<summary>
		/// Finds a field by id
		///</summary>
		///<param name="site">The site to start a search.</param>
		///<param name="id">The id of the field to find</param>
		///<param name="name">The name of the field to find</param>
		///<returns>the field or null if not found.</returns>
		public static SPField FindFieldByIdOrNameDefault(SPWeb site, Guid id, string name)
		{
			return FindInSiteOrParentSites(site, s => s.Fields.Cast<SPField>().FirstOrDefault(f => f.Id == id || f.InternalName == name));
		}

		/// <summary>
		/// Finds or adds a site group.
		/// </summary>
		/// <param name="site">The site to find or add the group</param>
		/// <param name="name">The name of the group</param>
		/// <param name="owner">The owner of the group</param>
		/// <param name="defaultUser">The default user in the group</param>
		/// <param name="description">The description of the group.</param>
		/// <returns>The group.</returns>
		public static SPGroup EnsureSiteGroup(SPWeb site, string name, SPMember owner, SPUser defaultUser, string description)
		{
			SPGroup group = FindSiteGroupByNameOrDefault(site, name);
			if (group == null)
			{
				site.SiteGroups.Add(name, owner, defaultUser, description);
				return site.SiteGroups[name];
			}
			return group;
		}

		/// <summary>
		/// Finds a site group or null if not found.
		/// </summary>
		/// <param name="site">The site to start the search.</param>
		/// <param name="name">The name of the site group</param>
		/// <returns>The group or null if not found.</returns>
		public static SPGroup FindSiteGroupByNameOrDefault(SPWeb site, string name)
		{
			return FindInSiteOrParentSites(site, s => s.SiteGroups.Cast<SPGroup>()
				.FirstOrDefault(g => string.Equals(g.Name, name, StringComparison.InvariantCultureIgnoreCase)));
		}

		/// <summary>
		/// Finds a user by login name or null if not found.
		/// </summary>
		/// <param name="site">The site to start the search</param>
		/// <param name="loginName">The login name of the user to find</param>
		/// <returns>The user or null if not found</returns>
		public static SPUser FindUserByLoginNameOrDefault(SPWeb site, string loginName)
		{
			return FindInSiteOrParentSites(site, s => s.Users.Cast<SPUser>()
				.FirstOrDefault(u => string.Equals(loginName, u.LoginName, StringComparison.InvariantCultureIgnoreCase)));
		}

		/// <summary>
		/// Finds a user by name or null if not found.
		/// </summary>
		/// <param name="site">The site to begin the search.</param>
		/// <param name="id">The id of the user to find.</param>
		/// <returns>The user or null if not found.</returns>
		public static SPUser FindUserByIdOrDefault(SPWeb site, int id)
		{
			return FindInSiteOrParentSites(site, s => s.Users.GetByID(id));
		}
	}
}
